Bun の非互換な拡張 API
https://gyazo.com/a71f94cf1c15e71313d8a2c16c59d1ab
Bun は WinterTC からの招待を無視し、標準から外れた拡張やまだプラットフォームで議論中の仕様を利便性のためだけに取り入れている。またエコシステムとして合意の取れていない実装をすることもある。
@jarredsumner: JS runtimes obsess about web standards but web standards orgs are incentivized to only care about browsers
https://pbs.twimg.com/media/GGjn7Q3a4AAXGKf.jpg
@lcasdev: @jarredsumner Just want to mention that we’ve invited you to WinterCG meetings for nearly 2 years now without any response from you - I think intentionally not participating and then saying “uh they don’t listen” is not very reasonable 😀
該当ツイートが消されているが、過去に「俺は幼稚園の徒競走みたいに手をつないでゴールをしたくない」と発言をしたこともある。
これら非互換な API を使ってしまうと書いたコードが Node.js や Deno、Cloudflare Workers などで扱えず相互運用性の問題となる。
また Bun の独自拡張を標準と勘違いしてしまい他ランタイムに対して誤ったバグ報告する例がいくつか見られ、JS コミュニティによくない影響が出てきている。これは WinterTC の目標を妨げている。
知らず知らずのうちに使ってしまわないようにまとめておく。なおバンドラーのため機能(マクロや CSS Modules など)についてはここでは取り扱わない。
ECMAScript (JavaScript)
ES Modules のキャッシュ操作
Node.js や Deno では ES Modules と CommonJS が明確に区別されている。一方で Bun はモジュールを扱いやすくするために CommonJS と ES Modules を意図的に混ぜている。その結果、ES Modules で読み込んだモジュールのキャッシュを require.cache で操作出来るようになってしまっている。
code: js
import { foo } from "./foo.mjs";
const path = Bun.fileURLToPath(import.meta.resolve("./foo.mjs"));
console.log(require.cachepath);
JavaScript の仕様では Source Text Module Records である JavaScript モジュール(ES Modules)のキャッシュ操作が出来ない。なお TC39 メンバーである Jordan Harband さんからも ECMAScript にそのような仕様はないと教えてもらった。
@jordan.har.band: CJS modules aren’t governed by the spec, and there’s no mechanism in the spec to alter a hypothetical ESM cache.
このような Bun の標準違反は TC39 内でも問題視されており、import.sync が提案された理由にそのような背景も含まれる。
ところで Node.js, Deno, Bun で CommonJS によって読み込んだモジュールに対して require.cache でキャッシュ操作が出来るが、これは Abstruct Module Records であると解釈することで ECMAScript 仕様違反ではないと理解することができる。この辺りは uhyo さんのスライドが詳しい。
https://speakerdeck.com/uhyo/require-esm-toecmascriptshi-yang
実行前トランスパイルで起きる仕様違反
翠さんによるまとめ。
https://zenn.dev/sapphi_red/scraps/90bb52d5a1d380
Web 標準 API
AsyncIterable な console で標準入力から一行ずつ読む
In Bun, the console object can be used as an AsyncIterable to sequentially read lines from process.stdin.
https://bun.sh/docs/api/console
code: js
for await (const line of console) {
console.log(line);
}
もちろんこの機能は Console Standard にはない。
DOMException のコンストラクタにオプションオブジェクトを渡す
本来 DOMException の第二引数は name プロパティを文字列で渡すことしかできないが、オブジェクトとして渡し、なおかつ cause プロパティも追加できるように拡張している。
code: js
// Create a DOMException with name and cause
const error = new DOMException("Something went wrong", {
name: "CustomError",
cause: new Error("Underlying cause"),
});
console.log(error.name); // "CustomError"
console.log(error.cause); // Error: Underlying cause
https://bun.sh/blog/bun-v1.2.13#domexception-supports-name-and-cause
DOMException は Web 標準 API が例外として投げるものであるから、通常 cause プロパティは付与されない。ナンセンス。
ReadableStream の text, json, blob, そして bytes メソッド
Streams Standard で Response にある text, json, blob そして bytes メソッドを ReadableStream にも追加しようという動きがある。
https://github.com/whatwg/streams/pull/1311
Bun はまだ Web 標準に追加されていないこの議論中の提案をすでに実装している。
code: js
// Before
const data = await Bun.readableStreamToJSON(stdout);
const data = await new Response(stdout).json();
// After
const data = await stdout.json();
https://bun.sh/blog/bun-v1.2.18#readablestream-text-json-bytes-blob
コード例にもあるように new Response でラップするべき。
ReadableStream の "direct" タイプ
type: "direct" を指定することで ReadableStream がクローズされたあとに値を要請した際のエラーで具体的な情報が表示されるようになるらしい。
Bun now provides more descriptive error messages when using ReadableStream with type: "direct". Instead of the generic "Expected Sink" error, you'll now see specific information about what went wrong, such as when trying to use a controller after the stream has been closed.
https://bun.sh/blog/bun-v1.2.5#improved-error-messages-for-readablestream
code: js
// Before: Generic "Expected Sink" error
// Now: Detailed error message
const stream = new ReadableStream({
type: "direct",
async pull(controller) {
controller.write("data");
// If you try to use the controller after pull() returns:
// Error: This HTTPResponseSink has already been closed.
// A "direct" ReadableStream terminates its underlying socket once async pull() returns.
},
});
標準の type: "bytes" と競合するため、同時に指定できない。どうするんだろう。
Response のコンストラクタに AsyncIterable を渡す
code: js
const response = new Response(async function* () {
yield "hello";
yield "world";
}());
await response.text(); // "helloworld"
https://bun.sh/docs/api/streams#async-generator-streams
これは Fetch Standard に沿っていない。ただし Node.js (undici) でも同じように実行できてしまう問題がある。
https://github.com/nodejs/node/issues/49551
Web 標準では ReadableStream.from を経由することで対処できる。
code: js
const response = new Response(ReadableStream.from(async function* () {
yield "hello";
yield "world";
}()));
await response.text(); // "helloworld"
https://github.com/whatwg/webidl/pull/1397
Headers の getAll と toJSON メソッド
Set-Cookie HTTP フィールド(ヘッダー)対応のために Fetch Standard の Headers に getAll メソッドを追加しようという議論が起きていた。最終的に Deno Land 社の Luca さんによる働きで getSetCookie メソッドを追加することとなった。
https://github.com/whatwg/fetch/issues/973
しかし Bun は結論が出る前に getAll と toJSON を独自実装した(toJSON をなぜ追加したのかはわからない)。
The Headers class now implements the .getAll() and .toJSON() methods. These are both technically non-standard methods, but we think it will make your life easier.
https://bun.sh/blog/bun-v0.3.0#new-methods-on-headers
code: js
const headers = new Headers();
headers.append("Set-Cookie", "a=1");
headers.append("Set-Cookie", "b=1; Secure");
console.log(headers.getAll("Set-Cookie")); // "a=1", "b=1; Secure"
console.log(headers.toJSON()); // { "set-cookie": "a=1, b=1; Secure" }
各ランタイムについての流れは Zenn に書いてある。
https://zenn.dev/pixiv/articles/3f607b19bb5e3a
Fetch API の body オプションに Async Generator Function を渡す
Fetch Standard では body オプションに渡せるのは Blob, BufferSource, FormData, URLSearchParams, string, ReadableStream のいずれかだが、Bun は Node.js API の Streams と Async Generator Functions を許容した。
You can now send fetch() Request bodies as streams. Previously, only Response bodies could be streamed in the client (Bun.serve() always supported both).
fetch()'s body supports streaming ReadableStream, Node.js streams and async generator* functions
https://bun.sh/blog/bun-v1.1.39#fetch-request-body-streams
code: js
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
fetch(url, {
method: "POST",
/* 'body' can be an async generator function */
body: async function* fn() {
const stream = client.messages.create(options);
for await (const {chunk: result} of stream) {
const { type, message } = chunk;
if (type === "succeeded") {
// stream message text chunk-by-chunk
yield message.content;
}
}
},
/* body can also be:
- body: new ReadableStream({...
- body: Bun.file(path)
- body: fs.createReadStream(path)
*/
);
前述した通り ReadableStream.from でラップするべき。
Fetch API の proxy オプション
In Bun, fetch supports sending requests through an HTTP or HTTPS proxy. This is useful on corporate networks or when you need to ensure a request is sent through a specific IP address.
https://bun.sh/guides/http/proxy
code: js
await fetch("https://example.com", {
// The URL of the proxy server
proxy: "https://username:password@proxy.example.com:8080",
});
Worker で type を指定せず ES Modules を扱う
You can use import and export syntax in your worker code. Unlike in browsers, there's no need to specify {type: "module"} to use ES Modules.
https://bun.com/docs/api/workers#creating-a-worker
Bun ではわざわざ type: "module" を指定せずに Worker で ES Modules が扱えることをあたかもメリットかのように扱っているが、互換性を考慮すると正しい選択だとは思えない。
Worker の "open" イベント
The "open" event is emitted when a worker is created and ready to receive messages. This can be used to send an initial message to a worker once it's ready. (This event does not exist in browsers.)
https://bun.sh/docs/api/workers#open
code: js
const worker = new Worker(new URL("worker.ts", import.meta.url).href);
worker.addEventListener("open", () => {
console.log("worker is ready");
});
Worker が起動されるまでに送られたメッセージはキューに入れられるため、特に待つ必要はない。
Worker の preload オプション
Worker を作る前にあらかじめ実行するコードを指定できるらしい。
Bun v1.1.35 introduces a preload option for Worker, which allows you to evaluate a script before the worker script is executed.
https://bun.sh/blog/bun-v1.1.35#preload-option-for-worker
code: js
const worker = new Worker(new URL("worker.js", import.meta.url).href, {
preload: new URL("preload.js", import.meta.url).href
});
Text Modules
WHATWG で Import Attributes を使った type: "text" の提案がなされている。エンコーディングをどう指定するかなどの議論が起きており、まだ仕様として定まっていない。
https://github.com/whatwg/html/issues/9444
Bun は結論を待たず、すでに独自実装している。
code: js
import html from "./index.html" with { type: "text" };
console.log(html);
https://bun.sh/blog/bun-v1.1.5#new-import-any-file-as-text
一方で Deno は議論中の機能ということでフラグを使って使用できる方向に舵を切った。
We’ve wanted to add importing other file types earlier, but also want to be aligned with the spec and avoid introducing breaking changes. Astute observers might note that this feature actually goes ahead of current spec. However, due to ongoing discussions and proposed upcoming features about this feature, we are confident this implementation is in the right direction.
https://deno.com/blog/v2.4#importing-text-and-bytes
Node.js
package.json で JSONC 形式の対応
突然 package.json で JSONC 形式の対応を表明したため賛否両論が巻き起こった。
@jarredsumner: In the next version of Bun
Bun won't error when package.json has comments or trailing commas
https://pbs.twimg.com/media/GLPRdS2asAAaMQh.png
PR にネガティブな意見が集まったが、最終的に Jarred 氏が Twitter 上のアンケート結果を根拠として Bun に取り込んだ。
https://bun.sh/blog/bun-v1.1.5#package-json-with-comments-and-trailing-commas
JSONC 形式の場合当然 npm にパブリッシュ出来ない(互換性がないのだから拡張子を jsonc にしてサポートした方がいいのにと個人的には思う)。
ちなみに pnpm は JSON5 と YAML をサポートしているがもちろん拡張子を分けている。
https://github.com/pnpm/pnpm/pull/1799
JSX
ショートハンド記法
code: jsx
function Div(props: {className: string;}) {
const {className} = props;
// without punning
return <div className={className} />;
// with punning
return <div {className} />;
}
https://bun.sh/docs/runtime/jsx#prop-punning
JSX の仕様は Meta (Facebook) により管理されている。
https://facebook.github.io/jsx/
特に仕様に対して提案するといったアプローチをとることなく拡張し、他ランタイムへ足並みを揃えるよう呼びかけず、急に TypeScript のパーサーへの対応を要求したため議論を呼んだ。リジェクトされた。
In bun's current implementation, we don't support multiple identifiers in one {}, just <div {foo} />. The rationale was to keep the parser changes as simple as possible and minimize edgecases. From a DX perspective, it would be better to support multiple but I think it's still net improvement to ship without supporting it
https://github.com/microsoft/TypeScript/issues/52057#issuecomment-1372709707
その他
TOML Modules
code: js
import data from "./data.toml";
// there's no toml extension, but type makes it read as toml.
import cfg from "./Configfile" with { type: "toml" };
https://bun.sh/guides/runtime/import-toml
https://bun.sh/blog/bun-v1.1.5#new-import-any-file-as-text
YAML Modules
@bunjavascript: In the next version of Bun
Native YAML support comes to JavaScript
Import, bundle, require and parse YAML in JS as easily as JSON
https://pbs.twimg.com/media/GzCXNYFbwAA-MoB.jpg